package com.asdhawaii.otello.board;

import com.asdhawaii.otello.OtelloPanel;
import com.asdhawaii.otello.util.ImageLoader;
import com.asdhawaii.otello.util.Timer;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.JPanel;

/**
 * The 8 x 8 playing board for Otello.
 * 
 * <pre>
 *    a b c d e f g h
 *   .---------------.
 * 0 | | | | | | | | |
 *   .---------------.
 * 1 | | | | | | | | |
 *   .---------------.
 * 2 | | | | | | | | |
 *   .---------------.
 * 3 | | | |B|W| | | |
 *   .---------------.
 * 4 | | | |W|B| | | |
 *   .---------------.
 * 5 | | | | | | | | |
 *   .---------------.
 * 6 | | | | | | | | |
 *   .---------------.
 * 7 | | | | | | | | |
 *   .---------------.
 * </pre>
 * 
 * Squares are referred to as [letter][number], though since this uses arrays 
 * [vertical number][horizontal number] is used in code.
 * 
 * @author Branden Ogata
 *
 */

@SuppressWarnings("serial")
public class BoardPanel extends JPanel
{  
  /**
   * The OtelloPanel in which this BoardPanel is located.
   * 
   */
  private OtelloPanel parent;
  
  /**
   * The squares on the board.
   * 
   */
  private Square[][] board;
  
  /**
   * The names of the squares on the board that are currently flipping.
   * 
   */
  private Set<String> flippingPieces;
  
  /**
   * The thread updating the images on the board.
   * 
   */
  private Thread refresh;
  
  /**
   * The number of squares on the x-axis.
   * 
   */
  private static final int MAX_X = 8;
  
  /**
   * The number of squares on the y-axis.
   * 
   */
  private static final int MAX_Y = 8;

  /**
   * Creates the starting Otello board.
   * 
   * @param parent The OtelloPanel in which this BoardPanel is located.
   * 
   */
  
  public BoardPanel(OtelloPanel parent)
  {
    this.parent = parent;
    this.setLayout(new GridLayout(8, 8));
    this.board = new Square[MAX_Y][MAX_X];
    ButtonListener buttonListener = new ButtonListener();
    
    // Set each square on the board to SquareContents.BLANK with the exception of the center four
    for (int i = 0; i < MAX_Y; i++)
    {
      for (int j = 0; j < MAX_X; j++)
      {
        // If there is a white or black piece in the starting position
        if (((i == 3) || (i == 4)) && ((j == 3) || (j == 4)))
        {
          // If the x and y coordinates are the same, then place a black piece
          // Else place a white piece
          this.board[i][j] = new Square((i == j) ? (SquareContents.BLACK) : (SquareContents.WHITE));
        }
        // Else a blank
        else
        {
          this.board[i][j] = new Square();
        }
        
        this.board[i][j].setName(String.format("%d%d", i, j));
        this.board[i][j].addActionListener(buttonListener);
        this.board[i][j].setVisible(true);
        this.add(this.board[i][j]);
      }
    }
    
    this.setVisible(true);
      
    this.flippingPieces = new HashSet<String>();
    this.refresh = new Thread(new BoardRefresher());
    this.refresh.start();
  }
  
  /**
   * Creates the starting Otello board without a reference to a containing object.
   * 
   */
  
  public BoardPanel()
  {
    this(null);
  }
  
  /**
   * Returns the board of this BoardPanel.
   *
   * @return this.board The Square[][] representing the board of this BoardPanel.
   *
   */
  
  public Square[][] getBoard()
  {
    return this.board;
  }

  /**
   * Places a new piece on the board, flipping other pieces as appropriate.
   * If the piece cannot legally be placed in those coordinates, 
   * then do not place the piece there.
   * 
   * @param row The int equal to the row coordinate of the piece to place.
   * @param column The int equal to the column coordinate of the piece to place.
   * @param color The SquareContents indicating the color of the piece to place.
   * 
   * @return success A boolean that is true if the placement was successful,
   *                                   false otherwise.
   *                                   
   */
  
  public boolean place(int row, int column, SquareContents color)
  {
    boolean success = false;
    
    // Consider placing the piece iff the square is unoccupied
    if (this.board[row][column].getContents().equals(SquareContents.EMPTY))
    {
      List<Direction> flips = isMoveValid(row, column, color);
      success = !flips.isEmpty();

      // If successful, then place the piece
      if (success)
      {
//        System.out.println("Added piece");
        this.board[row][column].setContents(color);

        for (Direction d : flips)
        {
          flip(row + d.getDeltaRows(), column + d.getDeltaColumns(), color, 
               d.getDeltaRows(), d.getDeltaColumns(), this.board[row][column].toString());
        }     
      }
    }
    
//    System.out.println(this);  
//    System.out.format("In BoardPanel.place, adding at (%d,%d) is %b%n", x, y, success);
    return success;
  }
  
  /**
   * Attempts to recursively flip pieces.  
   * 
   * @param row The int indicating the y coordinate of the current square.
   * @param column The int indicating the x coordinate of the current square.
   * @param color The SquareContents indicating the color of the piece to consider placing.
   * @param deltaRow The int indicating the change in y to go to look for the next piece.
   * @param deltaColumn The int indicating the change in x to go to look for the next piece.
   * @param stack The String indicating the pieces found thus far in the line under examination.
   * 
   * @return flipped A boolean that is true if a piece was flipped,
   *                                   false otherwise.
   */
  
  private boolean flip(int row, int column, SquareContents color, 
                       int deltaRow, int deltaColumn, String stack)
  {
    boolean flipped = false;
        
    // If the contents are empty or the coordinates are invalid, then stop without flipping
    if ((((column < 0) || (MAX_X <= column)) || ((row < 0) || (MAX_Y <= row))) || 
        (SquareContents.EMPTY.equals(this.board[row][column].getContents())))
    {
      flipped = false;
    }
    // If the contents are the same as the piece to place,
    // then stop and check if the placement is valid
    else if (color.equals(this.board[row][column].getContents()))
    {
      // If the stack contains an enemy piece, then flip pieces
      // The end points on the stack are necessarily color.toString(),
      // so the only issue is whether there are enemy pieces between them
      if (stack.contains(color.opposite().toString()))
      {
        flipped = true;
      }
    }
    // If the contents are opposite of the piece to place,
    // then continue in this direction
    else if (color.opposite().equals(this.board[row][column].getContents()))
    {
      flipped = flip(row + deltaRow, column + deltaColumn, color, deltaRow, deltaColumn,  
                     stack + this.board[row][column].toString());
      
      // If flipped, then change the contents to the opposite color piece
      if (flipped)
      {
        this.flippingPieces.add(this.board[row][column].getName());
        this.board[row][column].setContents(color);        
      }
    }    
    
    return flipped;
  }

  /**
   * Recursively determines if a move would flip pieces.  
   * 
   * @param row The int indicating the row coordinate of the current square.
   * @param column The int indicating the column coordinate of the current square.
   * @param color The SquareContents indicating the color of the piece to consider placing.
   * @param deltaRow The int indicating the change in column to go to look for the next piece.
   * @param deltaColumn The int indicating the change in rows to go to look for the next piece.
   * @param stack The String indicating the pieces found thus far in the line under examination.
   * 
   * @return flipped A boolean that is true if the piece would be flipped,
   *                                   false otherwise.
   */
  
  private boolean canFlip(int row, int column, SquareContents color, 
                          int deltaRow, int deltaColumn, String stack)
  {
    boolean flipped = false;
        
    // If the contents are empty or the coordinates are invalid, then stop without flipping
    if ((((column < 0) || (MAX_X <= column)) || ((row < 0) || (MAX_Y <= row))) || 
        (SquareContents.EMPTY.equals(this.board[row][column].getContents())))
    {
      flipped = false;
    }
    // If the contents are the same as the piece to place,
    // then stop and check if the placement is valid
    else if (color.equals(this.board[row][column].getContents()))
    {
      // If the stack contains an enemy piece, then flip pieces
      // The end points on the stack are necessarily color.toString(),
      // so the only issue is whether there are enemy pieces between them
      if (stack.contains(color.opposite().toString()))
      {
        flipped = true;
      }
    }
    // If the contents are opposite of the piece to place,
    // then continue in this direction
    else if (color.opposite().equals(this.board[row][column].getContents()))
    {
      flipped = canFlip(row + deltaRow, column + deltaColumn, color, deltaRow, deltaColumn, 
                        stack + this.board[row][column].toString());      
    }    
    
    return flipped;
  }  
  
  /**
   * Indicates if placing a piece at the given coordinates would be a valid move.
   * 
   * @param row The int indicating the row coordinate of the piece to place.
   * @param column The int indicating the column coordinate of the piece to place.
   * @param color The SquareContents indicating the color of the piece to place.
   * 
   * @return paths A List<Direction> containing the directions in which the move would flip pieces.
   *               If the list is empty, then no flips were found and the move is invalid.
   *                                 
   */
  
  private List<Direction> isMoveValid(int row, int column, SquareContents color)
  {
    List<Direction> paths = new ArrayList<Direction>();

    // If the square is empty
    if (this.board[row][column].getContents().equals(SquareContents.EMPTY))
    {
      // Check each possible direction
      for (Direction d : Direction.values())
      {
        // If the flip would not check the current location
        // (although flip would return false anyway, more efficient to stop that case here)
        if (((d.getDeltaColumns() != 0) || (d.getDeltaRows() != 0)) && 
            (canFlip(row + d.getDeltaRows(), column + d.getDeltaColumns(), color, 
                     d.getDeltaRows(), d.getDeltaColumns(), this.board[row][column].toString())))
        {
          paths.add(d);
        }
      }
    }
    
    return paths;
  }

  /**
   * Returns the number of pieces of a given color currently on this board.
   * 
   * @param color The SquareContents indicating the type of piece to search for.
   * 
   * @return pieces An int equal to the number of pieces of the given color.
   * 
   */
  
  public int getPieceCount(SquareContents color)
  {
    int pieces = 0;
    
    for (int i = 0; i < MAX_Y; i++)
    {
      for (int j = 0; j < MAX_X; j++)
      {
        if (this.board[i][j].getContents().equals(color))
        {
          pieces++;
        }
      }
    }
    
    return pieces;
  }
  
  /**
   * Indicates if there are any valid moves on the board for the given player.
   * 
   * @param color The SquareContents equal to the type of player to find valid moves for.
   * 
   * @return playable A boolean that is true if at least one valid move exists for the given player,
   *                                    false otherwise.
   *                                    
   */
  
  public boolean isPlayable(SquareContents color)
  {
    boolean playable = false;
    
    for (int i = 0; i < MAX_Y; i++)
    {
      for (int j = 0; j < MAX_X; j++)
      {
        playable = playable || !(isMoveValid(i, j, color).isEmpty());
      }
    }
    
    return playable;
  }

  /**
   * Finds the valid moves for the given color.
   * 
   * @param color The SquareContents indicating the color of the piece to find moves for.
   * 
   * @return candidates A List<Move> containing the valid moves on the board.
   * 
   */
  
  public List<Move> validMoves(SquareContents color)
  {
    List<Move> candidates = new ArrayList<Move>();
    
    for (int i = 0; i < this.board.length; i++)
    {
      for (int j = 0; j < this.board[i].length; j++)
      {
        if (!(isMoveValid(i, j, color)).isEmpty())
        {
          candidates.add(new Move(i, j));
        }
      }
    }
    
    return candidates;
  }
  
  /**
   * Resets the board to its starting position.
   * 
   */
  
  @SuppressWarnings("deprecation")
  public void reset()
  {
    for (int i = 0; i < MAX_Y; i++)
    {
      for (int j = 0; j < MAX_X; j++)
      {
        if (!this.board[i][j].getContents().equals(SquareContents.EMPTY))
        {
          this.board[i][j].setContents(SquareContents.EMPTY);
        }
      }
    }
    
    this.board[3][3].setContents(SquareContents.BLACK);
    this.board[4][4].setContents(SquareContents.BLACK);
    this.board[3][4].setContents(SquareContents.WHITE);
    this.board[4][3].setContents(SquareContents.WHITE);

    this.repaint();

    // Stop and restart the flipping thread
    this.refresh.stop();
    this.flippingPieces.clear();
    this.refresh = new Thread(new BoardRefresher());
    this.refresh.start();
  }
    
  /**
   * Indicates if any pieces are currently flipping on this BoardPanel.
   * 
   * @return True if pieces are flipping, 
   *         false otherwise.
   *         
   */
  
  public boolean isFlipping()
  {
    return !this.flippingPieces.isEmpty();
  }
  
  /**
   * Compares this BoardPanel to an object to test for equality.
   * This is true iff the parameter is a Board and all of the squares have the same contents.
   * 
   * @param obj The Object to compare this BoardPanel to.
   * 
   * @return isEqual A boolean that is true if this BoardPanel is equal to the parameter,
   *                                   false otherwise.
   *                                   
   */
  
  @Override
  public boolean equals(Object obj)
  {
    boolean isEqual = true;
    
    if ((this != obj) && (obj instanceof BoardPanel))
    {
      BoardPanel temp = (BoardPanel) obj;
      
      // Compare each square
      for (int i = 0; i < 7; i++)
      {
        for (int j = 0; j < 7; j++)
        {
          // If not equal, then the boards are unequal
          if (!this.board[i][j].equals(temp.board[i][j]))
          {
            isEqual = false;
          }
        }
      }
    }
    
    return isEqual;
  }
  
  /**
   * Returns a hash code based on this BoardPanel object.
   * Based on the suggestions in Effective Java, Chapter 3.
   * 
   * @return result An int equal to the hash code for this BoardPanel instance.
   * 
   */
  
  @Override
  public int hashCode()
  {
    int result = 17;
    int combination = 0;
    
    for (int i = 0; i < 7; i++)
    {
      for (int j = 0; j < 7; j++)
      {
        combination += this.board[i][j].hashCode();
      }
    }
    
    result = 11 * result + combination;
    return result;
  }
  
  /**
   * Returns a String representation of this BoardPanel.
   * Used mostly for debugging.
   * 
   * @return A String storing the contents of the 8x8 board.
   * 
   */
  
  @Override
  public String toString()
  {
    StringBuffer buffer = new StringBuffer();
    
    for (int i = 0; i < this.board.length; i++)
    {
      for (int j = 0; j < this.board[i].length; j++)
      {
        buffer.append(this.board[i][j]);
      }
      
      buffer.append('\n');
    }
    
    return buffer.toString();
  }
  
  /**
   * Handles button presses.
   * 
   */
  
  private class ButtonListener implements ActionListener
  {
    /**
     * Handles any button presses that do occur.
     * 
     * @param event An ActionEvent that recorded the button press.
     * 
     */
    
    @Override
    public void actionPerformed(ActionEvent event)
    {
      // If the source is a Square and there are no pieces flipping
      // (moves while pieces are flipping would result in errors)
      if ((event.getSource() instanceof Square) && (!isFlipping()))
      {
        Square square = (Square) event.getSource();
        boolean placed = false;

/*        
        System.out.println("Click in " + Thread.currentThread().getName() + " of " + 
                           Thread.activeCount() + " threads");
*/
        
        // Finds the row and column of the Square
        int row = Integer.valueOf(square.getName()) / 10;
        int column = Integer.valueOf(square.getName()) % 10;
/*        
        for (int i = 0; i < board.length; i++)
        {
          for (int j = 0; j < board[i].length; j++)
          {
            if (board[i][j] == square)
            {
              row = i;
              column = j;
            }
          }
        }
  */      
        // If the user clicked on an empty square, then try placing it
        if (square.getContents().equals(SquareContents.EMPTY))
        {
          SquareContents color = (parent.getTurn() % 2 == 0) ? (SquareContents.BLACK) : 
                                                               (SquareContents.WHITE);
          placed = place(row, column, color);

          if (placed)
          {
            square.doClick();
//            square.setEnabled(false);
            parent.nextTurn();
            
            // Update the scores
            parent.updateCount(getPieceCount(SquareContents.BLACK), 
                               getPieceCount(SquareContents.WHITE));
          }                
        }
        // Else if the square is not empty, refresh it
        else
        {
          square.setContents(square.getContents());
        }

//        square.setEnabled(true);
        
        // If the next player to move does not have a valid move, then move to the next player
        if (!isPlayable((parent.getTurn() % 2 == 0) ? (SquareContents.BLACK) : 
                                                      (SquareContents.WHITE)))
        {
          parent.nextTurn();
        }
      }
    }    
  }
  
  /**
   * Handles refreshing the icons on the board.
   * 
   */
  
  private class BoardRefresher implements Runnable
  {
    @Override
    public void run()
    {
      // If this is an actual Otello game and not just testing
      if (parent != null)
      {
        // Until the game and all animations are over
        while (((isPlayable(SquareContents.BLACK) || isPlayable(SquareContents.WHITE))) ||
               (!BoardPanel.this.flippingPieces.isEmpty()))
        {
          Set<String> toRemove = new HashSet<String>();
          
          // For each square that is flipping
          for (String flippingSquareName : BoardPanel.this.flippingPieces)
          {
            int row = Integer.valueOf(flippingSquareName.substring(0, 1));
            int column = Integer.valueOf(flippingSquareName.substring(1));
            
            Square current = BoardPanel.this.board[row][column];
            
            // If flipping, then update image
            if (current.isFlipping())
            {
              String imageName = current.getImageName();
              String baseName = imageName.substring(0, imageName.length() - 6);
              DecimalFormat formatter = new DecimalFormat("00");
              int imageNameLength = imageName.length();
              
              try
              {
                int nextFrame = Integer.valueOf(imageName.substring(imageNameLength - 6, 
                                                                    imageNameLength - 4)) + 1;
                String nextImageName;
                
                // If the next frame is less than 13, then go to the next frame
                // Files are in the format "[color]-[color]_##.jpg"
                if (nextFrame < 12)
                {
                  nextImageName = baseName + formatter.format(nextFrame) + ".jpg";
                }
                // Else switch to the first frame of the opposite color, then stop the animation
                else
                {
                  nextImageName = ((current.getContents().equals(SquareContents.BLACK)) ?
                                   ("black-white_s_") : ("white-black_s_")) + "00.jpg";
                  current.setFlipping(false);
                  toRemove.add(current.getName());
                }
                    
                current.setIcon(ImageLoader.getImageIcon(this, nextImageName));
                current.setImageName(nextImageName);
              }
              // If not a number, then start at 00
              catch (NumberFormatException e)
              {
                current.setIcon(ImageLoader.getImageIcon(this, baseName + "00.jpg"));
              }
              
              current.repaint();
              
              // Wait (128 / # of flipping pieces) seconds before moving on to the next frame
              Timer.pauseForMillis(128 / flippingPieces.size());
            }
          }
         
          if (!toRemove.isEmpty())
          {
            BoardPanel.this.flippingPieces.removeAll(toRemove);
          } 
          
          // If all flipping animations are complete, then make or allow the next move
          if (!BoardPanel.this.isFlipping())
          {
            // Make the next move
            parent.nextMove((parent.getTurn() % 2 == 0) ? (SquareContents.BLACK) : 
                                                          (SquareContents.WHITE));
          }
        }      
      }
    }
  }  
}